#pragma once

#include <memory>
#include <openssl/aes.h>
#include <openssl/x509.h>
#include <optional>
#include <string>

namespace crypto {

/**
 * @brief Generates length random bytes using a cryptographically secure pseudo random generator (CSPRNG)
 */
std::string random(int length);

/**
 * Encrypt the given msg using AES ecb at 128 bit
 *
 * @param msg: the message to be encrypted
 * @param enc_key: the key used for encryption
 * @param iv: optional, if not provided a random one will be generated
 * @param padding: optional, enables or disables padding
 * @return: the encrypted string
 */
std::string aes_encrypt_ecb(std::string_view msg,
                            std::string_view enc_key,
                            std::string_view iv = random(AES_BLOCK_SIZE),
                            bool padding = false);

/**
 * Decrypt the given msg using AES ecb at 128 bit
 *
 * @param msg: the message to be encrypted
 * @param enc_key: the key used for encryption
 * @param iv: optional, if not provided a random one will be generated
 * @param padding: optional, enables or disables padding
 * @return: the decrypted string
 */
std::string aes_decrypt_ecb(std::string_view msg,
                            std::string_view enc_key,
                            std::string_view iv = random(AES_BLOCK_SIZE),
                            bool padding = false);

/**
 * Encrypt the given msg using AES CBC at 128 bit
 *
 * @param msg: the message to be encrypted
 * @param enc_key: the key used for encryption
 * @param iv: optional, if not provided a random one will be generated
 * @param padding: optional, enables or disables padding
 * @return: the encrypted string
 */
std::string aes_encrypt_cbc(std::string_view msg,
                            std::string_view enc_key,
                            std::string_view iv = random(AES_BLOCK_SIZE),
                            bool padding = false);

/**
 * Decrypt the given msg using AES CBC at 128 bit
 *
 * @param msg: the message to be encrypted
 * @param enc_key: the key used for encryption
 * @param iv: optional, if not provided a random one will be generated
 * @param padding: optional, enables or disables padding
 * @return: the decrypted string
 */
std::string aes_decrypt_cbc(std::string_view msg,
                            std::string_view enc_key,
                            std::string_view iv = random(AES_BLOCK_SIZE),
                            bool padding = false);

/**
 * Encrypt the given msg using AES gcm at 128 bit
 *
 * @param msg: the message to be encrypted
 * @param enc_key: the key used for encryption
 * @param iv: optional, if not provided a random one will be generated
 * @param padding: optional, enables or disables padding
 * @return: A pair of: the encrypted string and the MAC tag
 */
std::pair<std::string, std::string> aes_encrypt_gcm(std::string_view msg,
                                                    std::string_view enc_key,
                                                    std::string_view iv = random(AES_BLOCK_SIZE),
                                                    int iv_size = -1,
                                                    bool padding = false);

/**
 * Decrypt the given msg using AES gcm at 128 bit
 *
 * @param msg: the message to be encrypted
 * @param enc_key: the key used for encryption
 * @param tag: The MAC tag, ensure that the data is not accidentally altered or maliciously tampered during transmission
 * @param iv: optional, if not provided a random one will be generated
 * @param padding: optional, enables or disables padding
 * @return: the decrypted string
 */
std::string aes_decrypt_gcm(std::string_view msg,
                            std::string_view enc_key,
                            std::string_view tag,
                            std::string_view iv = random(AES_BLOCK_SIZE),
                            int iv_size = -1,
                            bool padding = false);

/**
 * Will sign the given message using the private key
 * @param msg: the message to be signed
 * @param private_key: the key used for signing
 * @return: the signature binary data
 */
std::string sign(std::string_view msg, std::string_view private_key);

/**
 * Will verify that the signature for the given message has been generated by the public_key
 * @param msg: the message that was originally signed
 * @param signature: the signature data
 * @param public_key: the public key, used to verify the signature
 * @return: true if the signature is correct, false otherwise.
 */
bool verify(std::string_view msg, std::string_view signature, std::string_view public_key);

/**
 * @brief returns the SHA256 hash of the given str
 */
std::string sha256(std::string_view str);

/**
 * @brief converts the given input into a HEX string
 */
std::string str_to_hex(std::string_view input);

/**
 * @brief takes an HEX vector and returns a string representation of it.
 */
std::string hex_to_str(std::string_view hex, bool reverse = true);

} // namespace crypto

/**
 * @brief Wrappers on top of OpenSSL methods in order to deal with x509 certificates
 *
 * Adapted from: https://gist.github.com/nathan-osman/5041136
 */
namespace x509 {

using x509_ptr = std::shared_ptr<X509>;
using pkey_ptr = std::shared_ptr<EVP_PKEY>;

/**
 * @brief Generates a 2048-bit RSA key.
 *
 * @return EVP_PKEY*: a ptr to a private key
 */
pkey_ptr generate_key();

/**
 * @brief Generates a self-signed x509 certificate.
 *
 * @param pkey: a private key generated with generate_key()
 * @return X509*: a pointer to a x509 certificate
 */
x509_ptr generate_x509(pkey_ptr pkey);

/**
 * @brief Reads a X509 certificate string
 */
x509_ptr cert_from_string(std::string_view cert);

/**
 * @brief Reads a X509 certificate from file
 */
x509_ptr cert_from_file(std::string_view cert_path);

/**
 * @brief Reads a private key from file
 */
pkey_ptr pkey_from_file(std::string_view pkey_path);

/**
 * @brief Write cert and key to disk
 *
 * @param pkey: a private key generated with generate_key()
 * @param pkey_filename: the name of the key file to be saved
 * @param x509: a certificate generated with generate_x509()
 * @param cert_filename: the name of the cert file to be saved
 * @return true when both pkey and x509 are stored on disk
 * @return false when one or both failed
 */
bool write_to_disk(pkey_ptr pkey, std::string_view pkey_filename, x509_ptr x509, std::string_view cert_filename);

/**
 * @param pkey_filename: the name of the key file to be saved
 * @param cert_filename: the name of the cert file to be saved
 * @return true when both files are present
 */
bool cert_exists(std::string_view pkey_filename, std::string_view cert_filename);

/**
 * @return the certificate signature
 */
std::string get_cert_signature(x509_ptr cert);

/**
 * @param private_key: set to true if EVP_PKEY is a private key, set to false for public keys
 * @return the key content in plaintext
 */
std::string get_key_content(pkey_ptr pkey, bool private_key);

/**
 * @return the private key content
 */
std::string get_pkey_content(pkey_ptr pkey);

/**
 * @return the certificate in pem format
 */
std::string get_cert_pem(x509_ptr cert);

/**
 *
 * @return the certificate public key content
 */
std::string get_cert_public_key(x509_ptr cert);

/**
 * Checks that the paired_cert is a valid chain for untrusted_cert.
 * @return std::nullopt if the verification runs fine, else the error message
 */
std::optional<std::string> verification_error(x509_ptr paired_cert, x509_ptr untrusted_cert);
} // namespace x509